Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

11장. Object와 Companion Object

지금까지 우리는 클래스로 객체를 “찍어내는” 법을 배웠습니다.

클래스는 붕어빵 틀이고,
그 틀로 붕어빵(객체)을 여러 개 만들 수 있었습니다.

그런데 세상에는 이런 경우도 있습니다.

  • 프로그램 전체에서 딱 하나만 있으면 되는 것
  • 클래스에 딸려 있지만, 객체 하나하나와는 상관없는 것

이 장에서는 이런 상황을 코틀린이 어떻게 다루는지 배웁니다.
바로 object와 Companion Object입니다.

자바를 조금 아는 분이라면,
자바의 static이 자꾸 떠오를 텐데요.
그 비교도 이 장에서 함께 정리하겠습니다.


11.1 object: Singleton 객체

하나만 존재하는 객체

먼저 상황을 하나 떠올려 봅시다.

프로그램 전체에서 설정 값을 관리하는 무언가가 필요합니다.
그런데 이런 관리자는 여러 개일 이유가 없습니다.

관리자가 두 명이면 오히려 헷갈립니다.
“진짜 설정은 누가 들고 있지?” 하는 문제가 생깁니다.

이렇게 프로그램 전체에서
딱 하나만 존재해야 하는 객체를
싱글턴(singleton)이라고 부릅니다.

single(하나) + ton →
“단 하나뿐인 것“이라는 뜻입니다.

자바에서 싱글턴을 만들려면 꽤 번거로웠습니다.
생성자를 막고, 정적 필드를 두고,
그 필드를 돌려주는 메서드를 따로 만들어야 했습니다.

코틀린은 이 과정을 단어 하나로 끝냅니다.
바로 object입니다.

object로 Singleton 만들기

object는 클래스를 정의하는 동시에
그 객체를 딱 하나 만들어 줍니다.

말로만 들으면 헷갈리니 코드로 보겠습니다.

object AppConfig {
    val appName = "주문 시스템"
    var version = "1.0.0"

    fun printInfo() {
        println("$appName ($version)")
    }
}

class가 아니라 object라고 적은 점에 주목해 주세요.

여기서 중요한 차이가 있습니다.

  • 클래스는 AppConfig()처럼 만들어서 써야 한다
  • object는 만들 필요 없이 이름 그대로 바로 쓴다

사용법을 보겠습니다.

fun main() {
    AppConfig.printInfo()          // 주문 시스템 (1.0.0)

    AppConfig.version = "1.0.1"
    AppConfig.printInfo()          // 주문 시스템 (1.0.1)
}
주문 시스템 (1.0.0)
주문 시스템 (1.0.1)

AppConfig()라고 괄호를 붙이지 않았습니다.
object는 이미 만들어진 하나의 객체이기 때문입니다.

프로그램 어디에서 AppConfig를 불러도
언제나 똑같은 그 객체 하나를 가리킵니다.

자바의 Singleton과 비교하기

자바에서 같은 싱글턴을 만들면 이렇게 됩니다.

// 자바
public class AppConfig {
    public static final AppConfig INSTANCE = new AppConfig();

    private AppConfig() { }   // 밖에서 못 만들게 막기

    public void printInfo() {
        System.out.println("주문 시스템");
    }
}

밖에서 새로 만들지 못하도록 생성자를 막고,
INSTANCE라는 정적 필드를 하나 둡니다.

쓸 때는 AppConfig.INSTANCE.printInfo()처럼
INSTANCE를 거쳐야 합니다.

코틀린은 이 모든 준비를
object 한 단어로 대신합니다.

object AppConfig {
    fun printInfo() {
        println("주문 시스템")
    }
}

정리하면 이렇습니다.

구분자바코틀린
선언class + static INSTANCEobject
생성자 막기직접 private으로 막음자동으로 막힘
사용AppConfig.INSTANCE.xxxAppConfig.xxx

자바가 여러 줄로 하던 일을,
코틀린은 단어 하나로 표현합니다.

전역 상태와 Singleton의 위험성

object는 편리하지만,
편리한 만큼 조심해서 써야 합니다.

이유는 object 안의 var 때문입니다.

앞의 예제에서 versionvar였습니다.
그리고 프로그램 어디에서든 바꿀 수 있었습니다.

AppConfig.version = "2.0.0"   // 어디서든 이렇게 바꿀 수 있음

이렇게 프로그램 어디에서나 접근하고
바꿀 수 있는 값을
전역 상태(global state)라고 부릅니다.

전역 상태는 왜 위험할까요?
2장에서 만난 고민이 그대로 되살아나기 때문입니다.

  • 이 값이 대체 어디서 바뀐 거지?
  • 지금 이 값이 맞는 값이 맞나?

싱글턴은 딱 하나뿐이라,
여러 곳에서 동시에 손을 댈 수 있습니다.
값이 꼬이면 원인을 찾기가 아주 어렵습니다.

특히 서버는 여러 요청을 동시에 처리합니다.
싱글턴의 var를 여러 요청이 함께 바꾸면
예상치 못한 오류로 이어질 수 있습니다.

그래서 object를 쓸 때는 이렇게 기억해 둡시다.

object에는 되도록 바뀌지 않는 값(val)만 담자.
자주 바뀌는 상태는 싱글턴에 두지 말자.

object가 잘 어울리는 곳은
값이 거의 바뀌지 않는 경우입니다.

  • 고정된 설정 값
  • 여러 곳에서 함께 쓰는 상수 모음
  • 상태 없이 계산만 해 주는 도구

정리하면,
object는 “하나만 있으면 되는, 상태 없는 것“에
가장 잘 맞습니다.


11.2 Companion Object

클래스와 연결된 객체

이번에는 조금 다른 상황입니다.

클래스는 여전히 객체를 여러 개 만들어야 합니다.
그런데 그중에는 “객체 하나하나“가 아니라
“클래스 전체“에 속하는 것도 있습니다.

예를 들어 회원 클래스를 생각해 봅시다.

class Member(val name: String)

Member("홍길동"), Member("이순신")처럼
회원 객체는 여러 개 만들어집니다.

name은 회원마다 다릅니다.
홍길동의 이름과 이순신의 이름은 다르니까요.

그런데 이런 값은 어떨까요?

  • 회원 이름의 최대 길이 제한
  • 지금까지 만들어진 회원 수

이런 값은 회원 한 명의 것이 아닙니다.
“회원이라는 개념 전체“에 속하는 값입니다.

이렇게 특정 객체가 아니라
클래스 자체에 딸려 있는 것을 담는 공간이
바로 컴패니언 오브젝트(companion object)입니다.

companion은 “동반자, 짝꿍“이라는 뜻입니다.
클래스와 짝을 이루어 함께 다니는 객체라고
생각하면 쉽습니다.

companion object 사용하기

클래스 안에 companion object 블록을 넣습니다.

class Member(val name: String) {
    companion object {
        const val MAX_NAME_LENGTH = 20

        fun describe() {
            println("이름은 최대 ${MAX_NAME_LENGTH}자입니다.")
        }
    }
}

컴패니언 오브젝트 안의 값과 함수는
객체를 만들지 않고 클래스 이름으로 바로 씁니다.

fun main() {
    println(Member.MAX_NAME_LENGTH)   // 20
    Member.describe()                 // 이름은 최대 20자입니다.
}
20
이름은 최대 20자입니다.

Member()로 회원을 만들지 않고도
Member.MAX_NAME_LENGTH를 바로 썼습니다.

이 값은 회원 한 명의 것이 아니라
회원 클래스 전체의 것이기 때문입니다.

정리하면 이렇습니다.

  • 객체마다 다른 값 → 클래스의 프로퍼티 (name)
  • 클래스 전체에 하나뿐인 값 → 컴패니언 오브젝트

Factory Method

컴패니언 오브젝트가 가장 빛나는 곳은
객체를 만들어 주는 함수를 담을 때입니다.

이렇게 객체를 만들어서 돌려주는 함수를
팩토리 메서드(factory method)라고 부릅니다.

factory는 “공장“이라는 뜻입니다.
객체를 찍어내 주는 작은 공장이라고
생각하면 됩니다.

왜 굳이 이런 함수가 필요할까요?
예제로 이유를 만나 봅시다.

문자열 하나로 회원을 만들고 싶다고 해 봅시다.
"홍길동,20"처럼 이름과 나이가
쉼표로 붙어 있는 문자열입니다.

생성자에 이 로직을 다 넣으면 지저분해집니다.
이럴 때 팩토리 메서드를 씁니다.

class Member(val name: String, val age: Int) {

    companion object {
        fun from(text: String): Member {
            val parts = text.split(",")
            val name = parts[0]
            val age = parts[1].toInt()
            return Member(name, age)
        }
    }
}

from은 문자열을 받아서
그 안에서 회원 객체를 만들어 돌려줍니다.

사용하는 쪽은 아주 깔끔해집니다.

fun main() {
    val member = Member.from("홍길동,20")
    println("${member.name} / ${member.age}")
}
홍길동 / 20

팩토리 메서드의 장점을 정리해 보겠습니다.

  • 이름을 붙일 수 있다 (from, of, create 등)
  • 만드는 과정이 복잡해도 밖에서는 간단하게 쓴다
  • 상황에 따라 다른 방식으로 만들 수 있다

예를 들어 만드는 방법이 여러 가지라면
이름이 다른 함수를 여러 개 둘 수 있습니다.

class Member(val name: String, val age: Int) {

    companion object {
        fun of(name: String, age: Int): Member {
            return Member(name, age)
        }

        fun newborn(name: String): Member {
            return Member(name, 0)   // 나이는 0으로 고정
        }
    }
}

생성자만으로는 이렇게
“이름 있는 여러 방식“을 표현하기 어렵습니다.
생성자의 이름은 언제나 클래스 이름 하나뿐이니까요.

만드는 방법이 여러 가지이거나,
만드는 과정이 복잡할 때
팩토리 메서드를 떠올리면 됩니다.

이 방식은 나중에 DTO를 만들거나
문자열/데이터를 객체로 바꿀 때 자주 씁니다.
(DTO 변환은 33장에서 자세히 다룹니다.)


11.3 Java의 static과 비교

정적 메서드와 Companion Object

자바를 배운 분이라면
지금까지의 이야기가 익숙할 것입니다.

자바에서는 이런 것들을
static이라는 키워드로 표현했습니다.

정적(static)이란
“특정 객체가 아니라 클래스에 속한다“는 뜻입니다.

자바 코드로 보겠습니다.

// 자바
public class Member {
    public static final int MAX_NAME_LENGTH = 20;

    public static Member from(String text) {
        String[] parts = text.split(",");
        return new Member(parts[0]);
    }

    private final String name;

    public Member(String name) {
        this.name = name;
    }
}

static이 붙은 것들은
Member.MAX_NAME_LENGTH, Member.from(...)처럼
객체 없이 클래스 이름으로 바로 씁니다.

여기서 중요한 차이가 하나 있습니다.

코틀린에는 static이라는 키워드가 아예 없습니다.
그 자리를 컴패니언 오브젝트가 대신합니다.

두 언어를 나란히 비교하면 이렇습니다.

자바코틀린
static 필드companion object의 프로퍼티
static 메서드companion object의 함수
Member.from(...) 호출Member.from(...) 호출 (동일)

부르는 방식(Member.from)은 똑같습니다.
다만 코틀린은 그것을 static이 아니라
“클래스와 짝을 이루는 객체“로 표현합니다.

이 차이가 중요한 이유가 있습니다.
코틀린의 컴패니언 오브젝트는 진짜 “객체“라서,
인터페이스를 구현하는 등 더 많은 일을 할 수 있습니다.
(인터페이스는 8장에서 다뤘습니다.)

지금은 이렇게만 기억해 둡시다.

자바의 static이 하던 일을,
코틀린은 companion object로 한다.

const val

앞의 예제에서 이런 코드를 봤습니다.

companion object {
    const val MAX_NAME_LENGTH = 20
}

여기서 val 앞에 const가 붙었습니다.
const가 무엇인지 짚고 넘어가겠습니다.

2장에서 val
“한 번 정하면 바꿀 수 없는 값“이라고 배웠습니다.

const val은 거기서 한 걸음 더 나갑니다.
“컴파일하는 시점에 이미 정해지는 상수“입니다.

상수(constant)란
프로그램 내내 절대 바뀌지 않는 고정된 값입니다.

valconst val의 차이를 정리하면 이렇습니다.

  • val : 프로그램이 실행되면서 값이 정해질 수 있다
  • const val : 코드를 쓰는 순간 값이 이미 정해져 있다

그래서 const val에는 제약이 있습니다.

  • 숫자, 문자열, true/false 같은
    단순한 값만 담을 수 있다
  • 함수 호출의 결과 같은 것은 담을 수 없다
  • objectcompanion object,
    또는 파일 맨 바깥(톱레벨)에만 놓을 수 있다

간단한 예를 보겠습니다.

const val PI = 3.14                // 가능: 단순한 숫자
const val APP_NAME = "주문 시스템"   // 가능: 단순한 문자열

const val NOW = System.currentTimeMillis()
// 오류: 실행해 봐야 아는 값은 const가 될 수 없음

그럼 언제 const val을 쓸까요?
“절대 바뀌지 않는 고정 값“을 이름 붙일 때입니다.

class Order {
    companion object {
        const val MAX_ITEM_COUNT = 100
        const val STATUS_PAID = "PAID"
    }
}

이렇게 이름을 붙여 두면
숫자 100이나 문자열 "PAID"
코드 여기저기에 흩뿌리지 않아도 됩니다.

뜻 없는 숫자나 문자열 대신
이름 있는 상수를 쓰면
코드가 훨씬 읽기 쉬워집니다.

참고로 자바의 public static final
코틀린의 const val과 가장 가까운 짝입니다.


11.4 익명 객체

이름 없이 그 자리에서 만드는 객체

지금까지 본 object에는 모두 이름이 있었습니다.
AppConfig, Member.Companion처럼요.

그런데 때로는
이름을 붙일 필요조차 없는 객체가 있습니다.

딱 한 번, 그 자리에서만 쓰고 버릴 객체입니다.
이런 것을 익명 객체(anonymous object)라고 부릅니다.

익명(匿名)은 “이름을 숨긴다”,
즉 “이름이 없다“는 뜻입니다.

익명 객체는 주로
인터페이스를 그 자리에서 간단히 구현할 때 씁니다.

object : Interface로 구현체 만들기

먼저 간단한 인터페이스를 하나 준비하겠습니다.
(인터페이스는 8장에서 배웠습니다.)

interface Greeter {
    fun greet(): String
}

이 인터페이스를 쓰려면
greet()를 실제로 채운 구현체가 필요합니다.

보통은 이렇게 클래스를 따로 만듭니다.

class KoreanGreeter : Greeter {
    override fun greet(): String {
        return "안녕하세요"
    }
}

그런데 이 구현이
딱 한 곳에서 한 번만 필요하다면 어떨까요?
클래스를 따로 만드는 것이 오히려 번거롭습니다.

이럴 때 익명 객체를 씁니다.
object와 콜론(:)을 이어 붙여
그 자리에서 바로 구현합니다.

fun main() {
    val greeter = object : Greeter {
        override fun greet(): String {
            return "안녕하세요"
        }
    }

    println(greeter.greet())
}
안녕하세요

object : Greeter { ... } 부분을 봐 주세요.

  • object : 객체를 하나 만든다
  • : Greeter : 이 객체는 Greeter를 구현한다
  • { ... } : 그 자리에서 내용을 채운다

클래스에 이름을 붙여 따로 선언하지 않았습니다.
필요한 곳에서 바로 만들어 썼을 뿐입니다.

익명 객체는 언제 쓰는가

익명 객체가 어울리는 상황을 정리하면 이렇습니다.

  • 구현이 아주 짧다
  • 그 자리에서 딱 한 번만 쓴다
  • 이름을 붙여 재사용할 일이 없다

반대로, 같은 구현을 여러 곳에서 쓴다면
이름 있는 클래스로 만드는 편이 낫습니다.
그래야 재사용할 수 있으니까요.

자바를 아는 분이라면
익명 객체가 자바의 “익명 클래스“와
닮았다고 느낄 것입니다.

// 자바의 익명 클래스
Greeter greeter = new Greeter() {
    @Override
    public String greet() {
        return "안녕하세요";
    }
};

역할은 거의 같습니다.
자바의 익명 클래스가 하던 일을
코틀린은 object : ... 표현으로 합니다.

한 가지만 미리 언급해 두겠습니다.
함수가 하나뿐인 인터페이스라면,
익명 객체보다 더 짧은 방법도 있습니다.
바로 람다(lambda)입니다.
(람다는 14장에서 자세히 배웁니다.)

지금은 이렇게 기억해 두면 충분합니다.

이름 붙일 필요 없는
짧은 일회용 구현이 필요할 때
object : 인터페이스를 떠올린다.


11장을 마치며

이 장에서 우리는 다음을 배웠습니다.

  • object로 만드는 싱글턴,
    즉 프로그램에 하나뿐인 객체
  • 싱글턴의 var가 전역 상태가 되어
    위험해질 수 있다는 점
  • 클래스와 짝을 이루는 컴패니언 오브젝트와,
    그 안에 두는 팩토리 메서드
  • 자바의 static을 코틀린은
    companion object로 대신한다는 점
  • 고정된 값을 이름 붙이는 const val
  • 이름 없이 그 자리에서 만드는 익명 객체

이 장의 도구들은
“객체 하나하나“가 아니라
“클래스나 프로그램 전체“를 다루는 도구였습니다.

특히 두 가지를 기억해 두면 좋습니다.
“하나만 필요하면 object”,
그리고 “만드는 일을 맡기려면 팩토리 메서드“입니다.

이것으로 4부, 객체를 설계하는 코틀린을 마칩니다.
다음 부에서는 컬렉션과 함수형 프로그래밍으로
데이터를 다루는 세계로 넘어갑니다.